// Copyright (c) 2021 Presto Labs Pte. Ltd.
// Author: jhkim

#include <algorithm>
#include "coin2/strategy/linear_model/focus.h"

namespace coin2::strategy::linear_model {

void LmSubStrategy::UpdateSignal(
    int64_t timestamp,
    int64_t* first_feed_time,
    double midret_pred,
    std::optional<double> midtomed_pred,
    OrderExecutorSystem* oe,
    PredictionInterval* pred_interval,
    AggregatedSizer* asizer) {
  if (!bbo) return;
  if (!pholder) return;
  CHECK(oe);
  if (!oe->HasProduct(*pholder)) return;
  if (!oe->HasPosition(*pholder)) return;
  CHECK(!price_window_.empty());

  midret_pred *= focus_config.signal_multiplier();
  pred_interval->UpdateSignal(timestamp, midret_pred);

  double signal_sigma;
  if (focus_config.has_fixed_sigma()) {
    signal_sigma = focus_config.fixed_sigma();
  } else {
    double _mean;
    bool use_warmup = focus_config.has_lean_aggr_nsigma() || focus_config.has_lean_per_symbol_nsigma() || focus_config.has_buy_threshold_nsigma();
    if(!pred_interval->GetMeanStd(&_mean, &signal_sigma, use_warmup)) return;
  }

  if (focus_config.has_signal_mix_type() && midtomed_pred) {
    if (focus_config.signal_mix_type() == LmFocusSignalMixType::ABS_MAX) {
      midret_pred = (std::abs(midret_pred) > std::abs(*midtomed_pred)) ? midret_pred : *midtomed_pred;
    } else if (focus_config.signal_mix_type() == LmFocusSignalMixType::AVERAGE) {
      midret_pred = (midret_pred + *midtomed_pred) / 2;
    }
  }

  if (last_logged_ts_ == 0L) last_logged_ts_ = timestamp;
  if (last_logged_ts_ + 120'000'000'000L < timestamp) {
    LOG(INFO) << symbol->symbol_string() << ": signal=" << midret_pred;
    last_logged_ts_ = timestamp;
  }

  double min_quote = GetMinPosQuote(focus_config) * focus_config.max_concentration();
  double max_quote = GetMaxPosQuote(focus_config) * focus_config.max_concentration();
  double reserve_quote = GetQuoteFromPq(*pholder, bbo->askp, reserve);

  if (asizer->symbols_clear.count(symbol) > 0) {
    min_quote = reserve_quote;
    max_quote = reserve_quote;
  }

  double curr_pos = GetCurrPos(oe);

  trading_pnls_.UpdateTimeOnly(timestamp);
  holding_pnls_.UpdateTimeOnly(timestamp);

  double midp = 0.5 * (bbo->askp + bbo->bidp);

  double holding_pnl = curr_pos * midp;
  if (holding_pnls_.num_popped() > 0) {
    holding_pnl -= holding_pnls_.last_popped();
  } else if (!holding_pnls_.empty()) {
    holding_pnl -= holding_pnls_.frontelem();
  } else {
    holding_pnl = 0;
  }

  double pnl_q = 0;
  if (trading_pnls_.num_popped() > 0) {
    pnl_q = (trading_pnls_.sum() + holding_pnl) / midp;
  }

  double curr_pos_quote = GetQuoteFromPq(
      *pholder,
      bbo->askp,
      curr_pos);
  asizer->UpdatePosition(symbol, curr_pos_quote);
  auto bs = asizer->GetRemainingBuySellQuote();
  double aggr_norm_pos = asizer->GetAggregatedNormalizedPos(focus_config.use_reserve());

  double aggr_skew_bps;
  if (focus_config.has_lean_aggr_nsigma()) {
    aggr_skew_bps = -aggr_norm_pos * focus_config.lean_aggr_nsigma() * signal_sigma;
  } else {
    aggr_skew_bps = -aggr_norm_pos * focus_config.lean_aggr_bps();
  }

  double max_pos_config = util::GetQtyFromQuoteSafe(
      *pholder,
      price_window_.last_element(),
      price_window_.first_element(),
      max_quote);
  double min_pos_config = util::GetQtyFromQuoteSafe(
      *pholder,
      price_window_.last_element(),
      price_window_.first_element(),
      min_quote);

  double max_pos = max_pos_config;
  double min_pos = min_pos_config;

  double fixed_lot_size = (max_pos - min_pos) / focus_config.stack();
  double min_order_qty = util::GetQtyFromQuoteSafe(
      *pholder,
      price_window_.last_element(),
      price_window_.first_element(),
      focus_config.min_order_quote());

  bool sell_block = false;
  bool buy_block = false;

  double lot_size;
  if (focus_config.pos_by_signal()) {
    CHECK(!focus_config.use_reserve());
    // curr_norm_pos: -1 ~ 1
    // asserts |max_quote| >= |min_quote|
    double curr_norm_pos = curr_pos_quote / max_quote;
    double target_norm_pos = normalizeSignal(midret_pred, focus_config);
    bool is_buy_direction = curr_norm_pos < target_norm_pos;

    double pos_dist = std::abs(target_norm_pos - curr_norm_pos) * max_pos;
    lot_size = focus_config.lot_multiplier() * pos_dist / focus_config.stack();

    if (!focus_config.only_signal_direction()) {
      sell_block |= is_buy_direction;
      buy_block |= !is_buy_direction;
    } else if ((midret_pred > 0) != is_buy_direction) {
      sell_block = true;
      buy_block = true;
    }
  } else if (focus_config.lot_by_signal()) {
    lot_size = fixed_lot_size * std::abs(focus_config.lot_multiplier() * normalizeSignal(midret_pred, focus_config));
  } else {
    lot_size = fixed_lot_size;
  }

  if (focus_config.use_aggregated_pos()) {
    double remain_buy_qty = std::max(0., GetQtyFromQuote(*pholder, bbo->askp, bs.first));
    double remain_sell_qty = std::max(0., GetQtyFromQuote(*pholder, bbo->askp, bs.second));
    max_pos = std::min(curr_pos + remain_buy_qty, max_pos);
    min_pos = std::max(curr_pos - remain_sell_qty, min_pos);
  }

  if (focus_config.max_abspos_quote() > 0) {
    auto bs2 = asizer->GetRemainingAbsBuySellQuote(symbol);
    double remain_buy_qty = std::max(0., GetQtyFromQuote(*pholder, bbo->askp, bs2.first));
    double remain_sell_qty = std::max(0., GetQtyFromQuote(*pholder, bbo->askp, bs2.second));
    max_pos = std::min(curr_pos + remain_buy_qty, max_pos);
    min_pos = std::max(curr_pos - remain_sell_qty, min_pos);
  }

  if (pnl_q < 0) {
    min_pos -= focus_config.loss_penalty_ratio() * pnl_q;
    max_pos += focus_config.loss_penalty_ratio() * pnl_q;
  }

  double ne = focus_config.nsigma_edge();
  if (focus_config.symbolwise_adjust_exponent() != 0.0) {
    if (buy_turnover_.num_elements() >= 10 &&
        sell_turnover_.num_elements() >= 10 &&
        market_turnover_.num_elements() >= 10) {
      buy_turnover_.UpdateTimeOnly(timestamp);
      sell_turnover_.UpdateTimeOnly(timestamp);
      market_turnover_.UpdateTimeOnly(timestamp);
      double turnover = buy_turnover_.sum() + sell_turnover_.sum();
      double market_turnover = market_turnover_.sum();
      double participation_ratio = turnover / market_turnover;
      (asizer->pr_symbols).Update((asizer->symbol_index)[symbol], participation_ratio);
    }
    double pr = (asizer->pr_symbols).GetValue((asizer->symbol_index)[symbol]);
    if (pr > 0.0) {
      double factor = std::pow(pr / (asizer->pr_symbols).mean(),
          focus_config.symbolwise_adjust_exponent());
      if (focus_config.symbolwise_adjust_increase_only() && factor > 1.0) {
        factor = 1.0;
      }
      if (focus_config.symbolwise_adjust_nsigma_edge()) {
        ne *= factor;
      }
      if (focus_config.symbolwise_adjust_lot_size()) {
        lot_size /= std::pow(factor, 0.5);
      }
    }
  }

  oe->AdjustSize(*pholder, min_pos, max_pos, lot_size, min_order_qty);

  double signal_lb;
  double signal_ub;
  if (*first_feed_time == 0L) *first_feed_time = timestamp;
  if (timestamp - *first_feed_time < 30e9) return;
  if (pred_interval->GetNSigmaBound(
        midret_pred,
        ne,
        &signal_lb,
        &signal_ub,
        focus_config.nsigma_edge_cap())) {
    double curr_rel_pos = focus_config.use_reserve() ? (curr_pos - 0.5 * (min_pos_config + max_pos_config)) : curr_pos;
    double current_stack = curr_rel_pos / fixed_lot_size;

    double skew_bps;
    if (focus_config.has_lean_per_symbol_nsigma()) {
      skew_bps = -current_stack / focus_config.stack() * 2 * focus_config.lean_per_symbol_nsigma() * signal_sigma;
    } else {
      skew_bps = -current_stack * focus_config.lean_per_stack_bps();
    }

    double signal_skewed_lb = signal_lb;
    double signal_skewed_ub = signal_ub;
    if (skew_bps + aggr_skew_bps > 0) {
      // aggressive bid posting becomes less aggressive
      signal_skewed_lb += (skew_bps + aggr_skew_bps) * focus_config.lean_close_multiplier();
      signal_skewed_ub += skew_bps + aggr_skew_bps;
    } else {
      // aggressive ask posting becomes less aggressive
      signal_skewed_lb += skew_bps + aggr_skew_bps;
      signal_skewed_ub += (skew_bps + aggr_skew_bps) * focus_config.lean_close_multiplier();
    }
    // some hard coded variables yet
    ManageMmOrder(
        oe,
        *pholder,
        *bbo,
        timestamp,

        &bom,
        leverage_rate,

        midret_pred,
        signal_lb,
        signal_ub,
        signal_skewed_lb,
        signal_skewed_ub,
        signal_sigma,
        sell_block,
        buy_block,
        // edge bps
        focus_config);
  }
  oe->onPeriodicOrderCheck(*pholder, timestamp);
}


void LmSubStrategy::ClearPositionWoSignal(
    int64_t timestamp,
    OrderExecutorSystem* oe,
    AggregatedSizer* asizer) {
  if (!bbo) return;
  if (!pholder) return;
  CHECK(oe);
  if (!oe->HasProduct(*pholder)) return;
  if (!oe->HasPosition(*pholder)) return;

  double curr_pos = GetCurrPos(oe);
  double min_order_qty = pholder->product_info().tick_qty().submittable_min_qty();
  bool require_hard_close = false;
  if (std::abs(curr_pos) < min_order_qty) {
    // current position is zero
    if (oe->GetConfig(*pholder).side().is_china_style() &&
        (oe->oq(*pholder)->open_long_qty(pholder->product()) > 0 ||
        oe->oq(*pholder)->open_short_qty(pholder->product()) > 0)) {
      // need to hard close, move on.
      require_hard_close = true;
    } else {
      const auto* ask_inside_order = oe->GetInsideOrder(*pholder, OrderDirection::DIRECTION_SELL);
      const auto* bid_inside_order = oe->GetInsideOrder(*pholder, OrderDirection::DIRECTION_BUY);
      if (!bid_inside_order && !ask_inside_order) {
        if (!cleared) {
          cleared = true;
          LOG(INFO) << "Cleared: " << symbol->product_name();
          LOG(INFO) << "Min Order Qty: " << min_order_qty;
          LOG(INFO) << "Curr Pos: " << curr_pos;
        }
      } else {
        oe->CancelAllOrders(*pholder, OrderDirection::DIRECTION_BUY, timestamp);
        oe->CancelAllOrders(*pholder, OrderDirection::DIRECTION_SELL, timestamp);
      }
      return;
    }
  }

  cleared = false;
  LOG(INFO) << "Try clear due to not rank range: " << symbol->symbol_string();
  double curr_pos_quote = GetQuoteFromPq(
      *pholder,
      bbo->askp,
      curr_pos);
  asizer->UpdatePosition(symbol, curr_pos_quote);
  double min_quote = GetMinPosQuote(focus_config) * focus_config.max_concentration();
  double max_quote = GetMaxPosQuote(focus_config) * focus_config.max_concentration();
  double max_pos = util::GetQtyFromQuoteSafe(
      *pholder,
      price_window_.last_element(),
      price_window_.first_element(),
      max_quote);
  double min_pos = util::GetQtyFromQuoteSafe(
      *pholder,
      price_window_.last_element(),
      price_window_.first_element(),
      min_quote);
  double lot_size = std::min(std::abs(curr_pos), (max_pos - min_pos) / focus_config.stack_clear());
  if (require_hard_close) {
    lot_size = (max_pos - min_pos) / focus_config.stack_clear();
  }
  oe->AdjustSize(*pholder, reserve, reserve, lot_size, min_order_qty);
  auto order_dir =
    curr_pos <= 0 ? OrderDirection::DIRECTION_BUY : OrderDirection::DIRECTION_SELL;
  if (!focus_config.has_min_clear_order_quote() ||
      std::abs(curr_pos_quote) > focus_config.min_clear_order_quote()) {
    ManageClearOrder(
        oe,
        *pholder,
        *bbo,
        timestamp,
        lot_size,
        order_dir,

        &bom,
        leverage_rate,

        focus_config);
  }
  oe->onPeriodicOrderCheck(*pholder, timestamp);
  return;
}


void LmFocusLogic::onFeed(
    const Symbol* symbol,
    const FeedUpdate& upd,
    const ::fastfeature::FeedMessage& feed_msg,
    LinearModel* linear_model,
    int64_t* lm_feed_id,
    const int64_t* global_feed_id,
    LinearModelDelegateProto* delegate,
    bool simulation) {
  auto focus_config = lm_product_config_.focus_config();
  if (strats_.count(symbol) > 0) {
    strats_.at(symbol)->UpdateProduct(product_cache_, upd.product(), upd.timestamp());
    if (feed_msg.has_book()
        && feed_msg.book().ask_size() > 0
        && feed_msg.book().bid_size() > 0) {
      auto _bbo = Bbo {
          feed_msg.fetched_time(),
          feed_msg.exchange_time(),
          feed_msg.book().ask(0).price_float(),
          feed_msg.book().bid(0).price_float(),
      };
      strats_.at(symbol)->UpdateBbo(_bbo);
      if (focus_config.use_reserve()) {
        strats_.at(symbol)->ManageExecutorConfig(upd.timestamp());
      }
    }
    if (focus_config.symbolwise_adjust_exponent() != 0.0) {
      if (feed_msg.feed_type() == ::fastfeature::TRADE_FEED) {
        strats_.at(symbol)->UpdateTrade(
            feed_msg.trade().price_float(),
            feed_msg.trade().qty_float(),
            feed_msg.fetched_time());
      }
    }
  }

  bool is_prem_abnormal = false;
  if (focus_config.use_abnormal_premium_filter() > 0) {
    if (prem_neut_feat_idx_ == -1) {
      CHECK(linear_model->GetAllFeatures()->GetIndex("FocusStBasic.prem_neut", &prem_neut_feat_idx_));
    }

    double prem_neut_bps = linear_model->GetAllFeatures()->GetValue(prem_neut_feat_idx_) * 1e4;
    double threshold_bps = focus_config.abnormal_premium_threshold_bps();
    if (feed_msg.has_book() && (prem_neut_bps >= threshold_bps || prem_neut_bps <= -threshold_bps)) {
      is_prem_abnormal = true;
    }
  }

  if (asizer_.symbols_clear.count(symbol) > 0 || is_prem_abnormal) {
    // CHECK(strats_.count(symbol) > 0);
    if (strats_.count(symbol) == 0) return;
    strats_[symbol]->ClearPositionWoSignal(
        upd.timestamp(),
        oe_,
        &asizer_);
    return;
  }
  delegate->ClearProto();
  lholder_.TriggerModelWithLatencyRecording(linear_model, nullptr);
  // LOG(INFO) << linear_model->GetAllFeatures()->DebugString();
  if (delegate->response_.buy_model().empty()) return;
  auto signal_resp = delegate->response_.buy_model().Get(0);
  double buymodel_signal = 0.0;
  int n_models = delegate->response_.buy_model().size();
  for (int i = 0; i < n_models; i++) {
    buymodel_signal += delegate->response_.buy_model().Get(i).signal();
  }
  buymodel_signal /= n_models;

  std::optional<double> midtomed_pred;
  if (!delegate->response_.midtomed_model().empty()) {
    midtomed_pred = delegate->response_.midtomed_model().Get(0).signal();
  }

  auto focus_symbolstr = signal_resp.symbol();
  const Symbol* focus_symbol = linear_model->GetFeatureManager()
      ->GetSymbolManager()->FromSymbolString(focus_symbolstr);

  if (asizer_.symbols_clear.count(focus_symbol) == 0 &&
      strats_.count(focus_symbol) > 0 && pred_intervals_.count(focus_symbol) > 0) {
    auto pred_interval = use_prediction_interval_per_symbol ? pred_intervals_[focus_symbol].get() : &aggregated_pred_interval_;
    strats_[focus_symbol]->UpdateSignal(
        upd.timestamp(),
        &first_feed_time_,
        buymodel_signal,
        midtomed_pred,
        oe_,
        pred_interval,
        &asizer_);
  }
  if (!simulation) {
    lholder_.LogSignal(upd.timestamp(), delegate);
  }
}

}  // namespace coin2::strategy::linear_model
